package com.houhanzhi.aspect;

import com.houhanzhi.annotation.RepeatSubmit;
import com.houhanzhi.constant.RedisKey;
import com.houhanzhi.enums.BizCodeEnum;
import com.houhanzhi.exception.BizException;
import com.houhanzhi.interceptor.LoginInterceptor;
import com.houhanzhi.util.CommonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

// 定义一个切面类
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 一般使用redissonClient可以放弃掉StringRedisTemplate，当然也可以都使用
    @Autowired
    private RedissonClient redissonClient;


    /**
     * 定义 @Pointcut注解表达式:表示在哪里执行当前方法
     * 方式一：@annotation：当执行的方法上拥有指定的注解时生效（我们采用这）
     * 方式二：execution：一般用于指定方法的执行
     *
     * @param repeatSubmit
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }

    /**
     * 环绕通知, 围绕着方法执行
     *
     * @param joinPoint    连接点
     * @param repeatSubmit 自定义注解
     * @return
     * @throws Throwable
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     * <p>
     * 方式一：单用 @Around("execution(* com.houhanzhi.controller.*.*(..))")可以
     * 方式二：用@Pointcut和@Around联合注解也可以（采用这个）
     * <p>
     * <p>
     * 两种方式
     * 方式一：加锁 固定时间内不能重复提交
     * <p>
     * 方式二：先请求获取token，这边再删除token,删除成功则是第一次提交
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {

        // 获取前端请求
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        // 用于记录成功或失败
        boolean res = false;
        // 防重提交类型
        String limitType = repeatSubmit.limitType().name();

        if (limitType.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            // 方式一：方法参数
            final long lockTime = repeatSubmit.lockTime();
            // 访问ip
            final String ipAddr = CommonUtil.getIpAddr(request);
            // 目标方法签名
            final MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 目标方法
            final Method method = signature.getMethod();
            // 目标类
            final String className = method.getDeclaringClass().getName();
            // String.format("%s-%s-%s-%s", ipAddr, method, className, accountNo)生成的key太长，做一下MD5加密，缩短一下key，节省空间
            String key = String.format(RedisKey.SUBMIT_ORDER_PARAM_KEY, CommonUtil.MD5(String.format("%s-%s-%s-%s", ipAddr, method, className, accountNo)));

            // 加锁一: 直接基于 redis 操作。设置 key，如果存在在返回 false，设置成功则返回 true
            // res = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);

            // 加锁二: 基于 redis+lua脚本操作：之前是因为 key组成是 order:submit:accountNo, value是对应的token，所以需要先获取值，再判断，比较繁琐

            // 加锁三：分布式锁
            final RLock lock = redissonClient.getLock(key);
            res = lock.tryLock(0, // 尝试加锁，最多等待0秒，加锁不成功，直接返回
                    lockTime, // 上锁以后5秒自动解锁 [锁过期时间lockTime默认为5s, 可以自定义]
                    TimeUnit.SECONDS);
        } else if (limitType.equalsIgnoreCase(RepeatSubmit.Type.TOKEN.name())) {
            // 方式二：令牌形式
            String requestToken = request.getHeader("request-token");
            if(StringUtils.isEmpty(requestToken)){
                throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
            }

            /**
             * 提交表单的token key
             * 方式一：不用lua脚本获取再判断，之前是因为 key组成是 order:submit:accountNo, value是对应的token，所以需要先获取值，再判断
             * 方式二：可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成
             */
            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);
            res = redisTemplate.delete(key);
        }

        /**
         * redis中没有token，则说明重复提交
         */
        if (!res) {
            log.error("订单重复提交");
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_REPEAT);
        }

        log.info("目标方法执行前");
        Object object = joinPoint.proceed();
        log.info("目标方法执行后");
        return object;
    }
}
